#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<ctype.h>
#include<fcntl.h>

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define DELIM " \t"
#define EXIT_CODE 44

#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 2
#define APPEND_RDIR 3

//声明环境变量
extern char **environ;    
//定义一个数组用来存放输入的指令
char commandline[LINE_SIZE];
//定义一个命令行参数表
char *argv[ARGC_SIZE]={NULL};
int quit=0;
//保存子进程的退出码
int lastcode=0;
//保存当前路径
char pwd[LINE_SIZE];
//自定义环境变量表
char myenv[LINE_SIZE];
//保存重定向方式
int rdir =NONE;
//保存重定向的目标文件
char *rdirfilename;

const char *getusername(){
    return getenv("USER");
}

const char *gethostname(){
    return getenv("HOSTNAME");
}

//将当前路径存放到数组中
void getpwd(){
    getcwd(pwd,sizeof(pwd));
}


//重定向封装成函数
void check_rdir(char *cmd){
    char *pos = cmd;

    while(*pos){
        if(*pos == '>'){
            //如果是追加重定向
            if(*(pos+1) == '>'){
                *pos++ = '\0';
                *pos++ = '\0';
                while(isspace(*pos)) pos++;
                rdir = APPEND_RDIR;
                rdirfilename = pos;
                break;
            }
            //如果是输出重定向
            else{
                *pos++ = '\0';
                while(isspace(*pos)) pos++;
                rdir = OUT_RDIR;
                rdirfilename = pos;
                break;
            }
        }
        else if(*pos == '<'){
            //如果是输入重定向
            *pos = '\0';
            pos++;
            while(isspace(*pos)) pos++;
            rdir = IN_RDIR;
            rdirfilename = pos;
            break;
        }
        else{}

        pos++;
    }
}

//指令输入封装成函数
void Interact(char *cline,int size){
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);

    //输入指令
    char *s = NULL;
    s = fgets(cline,size,stdin);
    assert(s);
    //给s做一个应用场景 表示使用过 防止报警
    //void(s);

    //最后输入的回车符取消掉 回车符对应的下标是长度减一
    cline[strlen(cline)-1]='\0';

    //判断输入的指令是否是重定向
    check_rdir(cline);
}

//指令切割封装成函数
int Splitstring(char cline[], char *_argv[]){
    int i=0;
    _argv[i++] = strtok(cline, DELIM);
    //strtok函数 第一个参数表示要分割的字符串 第二个参数为分割字符 一次只能分割一次 知道目标字符串为空
    while(_argv[i++] = strtok(NULL, DELIM));
    //选项个数返回
    return i-1;
}

//普通命令执行封装成函数
void NormalExcute(char *_argv[]){
    pid_t id=fork();
    if(id < 0){
        perror("fork");
        return;
    }
    else if(id == 0){
        //让子进程执行命令
        int fd = 0;
        //如果是输入重定向
        if(rdir == IN_RDIR){
            fd = open(rdirfilename, O_RDONLY);
            dup2(fd,0);
        }
        else if(rdir == OUT_RDIR){
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC,0666);
            dup2(fd,1);
        }
        else if(rdir == APPEND_RDIR){
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND,0666);
            dup2(fd,1);
        }

        //程序替换
        //execvpe(_argv[0], _argv, environ);
        execvp(_argv[0],_argv);
        exit(EXIT_CODE);
    }
    else{
        //父进程等待
        int status = 0;
        pid_t ret=waitpid(-1, &status, 0);
        if(ret == id){
            lastcode=WEXITSTATUS(status);
        }
    }  
}

//内建命令执行封装成函数
int Buildcommand(char *_argv[], int _argc){
    //cd指令
    if(_argc ==2 && strcmp(_argv[0],"cd")== 0){
        //将当前路径更改为cd指令的选项1
        chdir(argv[1]);
        //更新数组中的当前路径
        getpwd();
        //更新环境变量中的路径
        sprintf(getenv("PWD"),"%s",pwd);
        //返回1 表示执行内建命令
        return 1;
    }
    //export指令
    else if(_argc == 2 && strcmp(_argv[0],"export")==0){
        //将新增的环境变量复制到自定义环境变量表中
        strcpy(myenv,_argv[1]);
        //在当前父进程中追加新的环境变量
        putenv(myenv);

        return 1;
    }
    //echo指令
    else if(_argc ==2 && strcmp(_argv[0],"echo")==0){
        //如果选项1是字符串"$?",打印最近一个程序的退出码
        if(strcmp(_argv[1],"$?")==0){
            printf("%d\n",lastcode);
            //重置退出码
            lastcode=0;
        }
        //如果选项1的第一个字符是$
        if(*_argv[1] == '$'){
            //打印目标环境变量的值
            char *val = getenv(_argv[1]+1);
            if(val) printf("%s\n",val);
        }
        //如果不是直接打印选项1的内容
        else{
            printf("%s\n",_argv[1]);
        }

        return 1;
    }

    //ls指令特殊处理
    if(strcmp(_argv[0],"ls")==0){
        //如果是ls指令，在后面再追加一个选项--color
        _argv[_argc++]="--color";
        //将数组最后一个位置从新置为空指针
        _argv[_argc]=NULL;
    }


    //如果不是内建命令返回0
    return 0;
}

int main(){
    //shell是一个死循环
    while(!quit){
        //1.重置重定向信息
        rdir = NONE;
        rdirfilename = NULL;
        //2.交互问题，获取命令行
        Interact(commandline,sizeof(commandline));
    
        //commandlie -> ls -a -l -n\0 -> "ls" "-a" " -l" "-n"
        //3.命令行分割的问题，解析命令行
        int argc=0;
        argc = Splitstring(commandline,argv);
        //如果没有输入指令，结束本次循环，重新输入指令
        if(argc==0){
            continue;
        }

        //debug测试指令分割
        //int i=0;
        //for(;argv[i];i++){
        //    printf("argv[%d]:%s\n",i,argv[i]);
        //}

        //4.指令的判断
        //判断是否是内建命令
        int n = Buildcommand(argv,argc);

        //5.普通命令执行
        //如果不是内建命令，在执行普通命令
        if(n==0) NormalExcute(argv);
    }

    return 0;
}
